/*
 *  Copyright (C) 2004 Cidero, Inc.
 *
 *  Permission is hereby granted to any person obtaining a copy of 
 *  this software to use, copy, modify, merge, publish, and distribute
 *  the software for any non-commercial purpose, subject to the
 *  following conditions:
 *  
 *  The above copyright notice and this permission notice shall be included
 *  in all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
 *  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
 *  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY IN CONNECTION WITH THE SOFTWARE.
 * 
 *  File: $RCSfile: SyncShoutcastGroup.java,v $
 *
 */

package com.cidero.server;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Vector;
import java.util.ArrayList;
import java.util.logging.Logger;

import com.cidero.http.*;
import com.cidero.util.ByteBufferQueue;
import com.cidero.util.ShoutcastOutputStream;
import com.cidero.util.MrUtil;
import com.cidero.util.AppPreferences;
import com.cidero.server.roku.*;

/**
 *
 * Synchronous shoutcaster group class. Outputs streaming shoutcast-style data 
 * to a group of one or more clients in a (hopefully) synchronized manner.
 *
 * Synchronization Strategy
 *
 * When two or more clients of the same device type make a request of 
 * the server within ~4 seconds of one another, they are put in the 
 * same group, and the server begins serving data to them at the same 
 * time. If a round-robin scheme with fairly small data chunks is used, this
 * seems to work acceptably (sometimes the sync is less perfect than others,
 * and it is probably dependent on the device type as well).
 *
 * Note that this strategy doesn't work for devices of different types, or
 * perhaps not even devices with different firmware revisions.  It's all 
 * dependent on the devices having the same internal buffering & latency.
 *
 * Also, even for devices of the same type, there will be inter-device
 * drift due to small differences in the clocking of the device's DAC.
 * In a test of two particular Roku Soundbridges, for example, the sync
 * starts off well, then after 2 hours or so of listening to an Internet
 * radio station one can notice an echo effect.  The stream has to be
 * stopped and restarted to correct for this.
 * 
 * Drift Compensation  (Ideas for fixing above - not yet implemented)
 *
 * Right now the sockets used to send data to the device are configured as
 * blocking. If this was changed to use non-blocking mode, it may be possible
 * to figure out where each device is in the stream (write to the 
 * socket until an EWOULDBLOCK status is returned) and make small adjustments
 * by dropping data for devices that are 'behind', time-wise.
 *
 * The relative time position calculation would need to be filtered in
 * order to produce a reliable estimate. An 'outlier' rejection front-end,
 * followed by a simple smoothing stage with a ~5min time constant would
 * probably do the trick.
 *
 * When dropping some bytes of data, it would probably help to parse the
 * MP3 (or whatever format) frames and drop data at the end of the frame.
 * If the MP3 frame header itself was dropped, then the codec must drop
 * the whole frame (on the order of 400-500 bytes is a vague recollection)
 * and that might result in an over-correction.  Note that I don't really
 * know how the MPEG decoders work, so this is all conjecture!
 *
 */
public class SyncShoutcastGroup
{
  private static Logger logger = Logger.getLogger("com.cidero.server");

  HTTPProxySession parentSession;

  // Use vector for stream list since it is synchronized
  Vector  streamInfoList = new Vector();   
  boolean running = false;

  boolean enableSoundbridgeMetadataDisplay = false;

  class StreamInfo
  {
    ShoutcastOutputStream stream;
    HTTPConnection connection;

    // Roku soundbridge sketch control connection
    SBSketcher soundbridgeSketcher;  

    public StreamInfo( ShoutcastOutputStream stream,
                       HTTPConnection connection )
    {
      this.stream = stream;
      this.connection = connection;
    }

    public ShoutcastOutputStream getStream() {
      return stream;
    }

    public HTTPConnection getConnection() {
      return connection;
    }

    public void setSoundbridgeSketcher( SBSketcher sbSketcher ) {
      soundbridgeSketcher = sbSketcher;
    }
    public SBSketcher getSoundbridgeSketcher() {
      return soundbridgeSketcher;
    }
  }
  
  public SyncShoutcastGroup( HTTPProxySession parentSession,
                             ShoutcastOutputStream stream,
                             HTTPConnection connection,
                             boolean enableSoundbridgeMetadataDisplay )
  {
    this.parentSession = parentSession;

    StreamInfo streamInfo = new StreamInfo( stream, connection );
    this.enableSoundbridgeMetadataDisplay = enableSoundbridgeMetadataDisplay;

    String reqHost = connection.getRemoteAddress();

    if( enableSoundbridgeMetadataDisplay )
    {
      logger.fine("!!!!!!!!Enabling SB metadata display for host: " +
                  reqHost );
      SBSketcher sbSketcher = new SBSketcher( reqHost );
      streamInfo.setSoundbridgeSketcher( sbSketcher );
      //sbSketcher.enterSketchMode();
    }
    else
    {
      logger.fine("!!!!!NOT Enabling SB metadata display for host: " +
                  reqHost );
    }
    
    streamInfoList.add( streamInfo );
  }
  
  /*
   *  Add a new stream to the synchronzed group. This function may be
   *  invoked by other threads
   */
  public synchronized void addStream( ShoutcastOutputStream stream,
                                      HTTPConnection connection )
  {
    StreamInfo streamInfo = new StreamInfo( stream, connection );
    String reqHost = connection.getRemoteAddress();
    if( enableSoundbridgeMetadataDisplay )
    {
      logger.fine("Enabling SB metadata display for host: " + reqHost );
      SBSketcher sbSketcher = new SBSketcher( reqHost );
      streamInfo.setSoundbridgeSketcher( sbSketcher );
    }
    
    streamInfoList.add( streamInfo );
    logger.info("Added device to synchronized group - nDevices = " +
                streamInfoList.size() );
  }

  public void stop() {
    running = false;
  }
  
  /**
   *  Run a synchronous shoutcast session. This normally runs in the
   *  context of the HTTP-GET request thread of the 1st requester in the
   *  group.
   *
   *  The run routine returns when end of input from master server
   *  detected, or if *all* clients disconnected
   */ 
  public void run()
  {
    running = true;
    
    //
    //  Attach to queue with streaming data.
    //
    ByteBufferQueue queue = parentSession.getQueue();

    ByteBuffer qBuf;
    byte[] buf = new byte[65536];
    long totalBytes = 0;
    long lastTotal = 0;
    StreamInfo streamInfo = (StreamInfo)streamInfoList.get(0);
    ShoutcastOutputStream stream = streamInfo.getStream();

    int bytes;

    String streamTitle = "StreamTitle='Starting Total Bytes 0';";
    String lastStreamTitle = null;

    logger.info(" SyncShoutGroup: starting xfers, nClients = " +
                streamInfoList.size() );

    while( running )
    {
      try 
      {
        while( (qBuf = queue.get( 5000 )) == null )
        {
          logger.info("SyncShoutcastGroup: 5-sec timeout reading queue " );
          if( !running ) 
            return;
        }
        
        // qBuf.limit() contains the number of bytes actually received
        bytes = qBuf.limit();
        qBuf.get( buf, 0, bytes );  // copy to local buf
      }
      catch( IOException e )
      {
        logger.warning("SyncShoutcastGroup: Queue EOF!");
        running = false;
        break;
      }
      
      totalBytes += bytes;

      // Simulated shoutcast metadata experimentation
      /*
      if( (totalBytes - lastTotal) > 65536 )
        streamTitle = "StreamTitle='Total Bytes - " + totalBytes + "';";
      */
        
      try 
      {
        // Experimenting with sending data in 1024-byte chunks to improve
        // sync resolution (probably not necessary)
        for( int m = 0 ; m < 4 ; m++ )
        for( int n = 0 ; n < streamInfoList.size() ; n++ )
        {
          streamInfo = (StreamInfo)streamInfoList.get(n);
          stream = streamInfo.getStream();
          
          //
          // Write packet out to HTTP client.
          // 
          stream.write( buf, m*1024, 1024 );
          stream.flush();
          //stream.write( buf, 0, bytes );  // the old way - 4096 chunks
        }

      }
      catch( IOException e )
      {
        logger.info("Renderer closed connection" );

        // remove the offending (presumably closed) stream
        streamInfoList.remove( streamInfo );
        if( streamInfoList.size() == 0 )
          running = false;
      }

      if( (totalBytes - lastTotal) > 65536*2 )
      {
        logger.fine("SyncShoutcast Server: totalBytes = " + totalBytes );
        lastTotal = totalBytes;
      }
              
      //System.out.println("Proxy server - freeing packet");
      queue.free( qBuf );

    } // while ( running )
    
    //
    // Done - close off the connections
    //
    close();
  }
  
  public void close()
  {
    for( int n = 0 ; n < streamInfoList.size() ; n++ )
    {
      StreamInfo streamInfo;

      streamInfo = (StreamInfo)streamInfoList.get(n);

      HTTPConnection connection = streamInfo.getConnection();
      logger.fine("SyncShoutcastGroup: closing connection" );
      connection.close();

      SBSketcher sbSketcher = streamInfo.getSoundbridgeSketcher();
      if( sbSketcher != null )
      {
        logger.fine("SyncShoutcastGroup: closing SB Sketch connection" );
        sbSketcher.quitSketchMode();
        sbSketcher.close();
      }
    }

    logger.info("shutting down session" );
  }

  String lastMetadata = "dummy";
  long lastMetadataTime = 0;
  
  public void processMetadata( String metadata )
  {
    StreamInfo streamInfo;
    ShoutcastOutputStream stream;

    logger.fine("Processing Metadata:" + metadata );

    AppPreferences pref = RadioServer.getPreferences();

    long currTime = System.currentTimeMillis();

    if( metadata.equals( lastMetadata ) )
    {
      logger.finer("Metadata unchanged");
      long timeSinceLastUpdate = currTime - lastMetadataTime;
      if( timeSinceLastUpdate < pref.getInt("forcedMetadataUpdatePeriodMillis",
                                            120000 ) )
      {
        return;
      }
    }
    
    lastMetadata = metadata;
    lastMetadataTime = currTime;
    
    for( int n = 0 ; n < streamInfoList.size() ; n++ )
    {
      streamInfo = (StreamInfo)streamInfoList.get(n);
      stream = streamInfo.getStream();

      // Pass metadata along to device via output stream. It is inserted
      // every 8192 bytes using the proxy's icy-metaint value of 8192.
      // This may be different than the incoming frequency!
      stream.setMetadata( metadata );
      
      if( enableSoundbridgeMetadataDisplay )
      {
        SBSketcher sbSketcher = streamInfo.getSoundbridgeSketcher();

        if( sbSketcher != null )
        {
          // Fire off thread to update the sketch display. Thread is 
          // deferred for 30 sec so user can use Soundbridge remote
          // to control it (remote is locked out when SB in 'sketch' mode)
          SBMetadataUpdateThread sbMetadataUpdateThread =
            new SBMetadataUpdateThread( sbSketcher, metadata );
          sbMetadataUpdateThread.start();
        }
        else
        {
          logger.warning("sbSketcher = NULL!");
        }
      }
    }
  }

  class SBMetadataUpdateThread extends Thread
  {
    SBSketcher sbSketcher;
    String metadata;
    
    public SBMetadataUpdateThread( SBSketcher sbSketcher, String metadata )
    {
      this.metadata = metadata;
      this.sbSketcher = sbSketcher;
    }
    
    public void run()
    {
      AppPreferences pref = RadioServer.getPreferences();

      int delay = pref.getInt( "soundBridgeSketchModeDelayMillisec", 30000 );
      
      logger.fine("Outputting metadata after " + delay + " (ms) - " +
                  metadata );

      sbSketcher.quitSketchMode();
      MrUtil.sleep( delay );

      if( !running )
      {
        logger.info("SyncShoutcastGroup no longer running - exiting thread");
        return;
      }
        
      if( ! sbSketcher.enterSketchMode() )
      {
        logger.warning("Couldn't enter sketch mode - not updating metadata");
        return;
      }
      
      String[] metadataFields = metadata.split(";");
          
      for( int m = 0 ; m < metadataFields.length ; m++ )
      {
        if( metadataFields[m].toLowerCase().indexOf("title") >= 0 )
        {
          logger.finer("Found title = " + metadataFields[m] );
          int index = metadataFields[m].indexOf("=");
          if( index > 0 )
          {
            String title = metadataFields[m].substring( index+1 );
            // Strip off single-quotes, if present
            title = title.substring(1, title.length()-1 );
            logger.finer("title substring = " + title );
            //sbSketcher.enterSketchMode();
            sbSketcher.clear();

            int sepIndex = title.indexOf(" - ");
            if( sepIndex > 0 )
            {
              int endIndex;

              String artistLinePrefix = 
                pref.get("soundBridgeArtistLinePrefix", "");

              if( artistLinePrefix.indexOf("\"") >= 0 ) // Strip quotes
              {
                endIndex = artistLinePrefix.length()-1;
                artistLinePrefix = artistLinePrefix.substring(1, endIndex);
              }
              
              String titleLinePrefix = 
                pref.get("soundBridgeTitleLinePrefix", "");
      
              if( titleLinePrefix.indexOf("\"") >= 0 ) // Strip quotes
              {
                endIndex = titleLinePrefix.length()-1;
                titleLinePrefix = titleLinePrefix.substring(1, endIndex);
              }

              String artist = title.substring(0,sepIndex);
              sbSketcher.text( 0, 0, artistLinePrefix + artist );
              String songTitle = title.substring(sepIndex+3);
              sbSketcher.text( 0, 9, titleLinePrefix + songTitle );
            }
            else
            {
              sbSketcher.text( 0, 4, title );
            }
            
            // sbSketcher.quitSketchMode();
          }
        }
        break;
      }

      System.out.println("updateMetadataThread - exiting" );
    }

  }
  
}
